import socket
from threading import Thread

class P2PServer(object):
    """
    server class, which provides the core functionality
    to let others connect to your node's server, enabling you
    to receive data
    """
    
    #def __init__(self, onreceive, onconnect, onclose, ipv6: bool=False, buffersize: int=1024):
    def __init__(self, onreceive, onconnect, onclose, ipv6=False, buffersize=1024):
        """
        initialize the class members as necessary
        """
        
        self.serverport = None
        self.serversocket = None
        self.serverthread = None
        self.serverrunning = False
        
        self.clients = {}
        self.onreceive = onreceive
        self.onconnect = onconnect
        self.onclose = onclose
        self.buffersize = buffersize
        
        self.ipv6 = ipv6
    
    def __server_thread(self):
        # create a new socket
        if self.ipv6:
            self.serversocket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
        else:
            self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # bind the socket
        self.serversocket.bind((socket.gethostname(), self.serverport))
        # start listening
        self.serversocket.listen(5)
        # set running state True
        self.serverrunning = True
        
        # start mainloop
        while self.serverrunning:
            # accept a new connection
            (clientsocket, clientaddress) = self.serversocket.accept()
            # create a client thread
            clientthread = Thread(target=self.__client_thread, args=(clientsocket, clientaddress))
            clientthread.daemon = True
            #clientthread.daemon = True
            # add it to the clients dict
            self.clients[clientsocket] = {}
            self.clients[clientsocket]["thread"] = clientthread
            self.clients[clientsocket]["address"] = clientaddress
            # trigger new connection target
            self.onconnect(self.clients[clientsocket]["address"], self.send);
            # start client thread
            clientthread.start()
    
    #def __client_thread(self, clientsocket: socket, clientaddress: str):
    def __client_thread(self, clientsocket, clientaddress):
        # running state for the client socket
        clientrunning = True
        # loop until the connection is lost
        while clientrunning:
            # blocking receive method
            data = clientsocket.recv(self.buffersize)
            # trigger targeted method with the data
            self.onreceive(clientaddress, data)
            # if no data has arrived the socket is broken
            if data == '':
                clientrunning = False
                # trigger closed connection target
                self.onclose(clientaddress)
                self.__remove_client_from_dict(clientsocket)
    
    #def __get_clientsocket_by_address(self, address: str):
    def __get_clientsocket_by_address(self, address):
        clientsocket = None
        
        for key in self.clients:
            if self.clients[key]["address"] == address:
                clientsocket = key
        
        return clientsocket
    
    #def __remove_client_from_dict(self, clientsocket: socket):
    def __remove_client_from_dict(self, clientsocket):
        # client is disconnected, so remove it from the clients dict
        del self.clients[clientsocket]
    
    
    #def contains(self, address: str):
    def contains(self, address):
        if self.__get_clientsocket_by_address(address) == None:
            return False
        else:
            return True
    
    #def start(self, port: int):
    def start(self, port):
        """
        set the server into serving mode, so
        clients will be able to connect
        """
        
        self.serverport = port
        # create the server thread 
        self.serverthread = Thread(target=self.__server_thread)
        self.serverthread.daemon = True
        self.serverthread.start()
    
    def stop(self):
        """
        stop the server, all clients will be
        disconnected
        """
        
        # set the running state False
        self.serverrunning = False
        # wait 0.1 seconds until killing the thread
        self.serverthread.join(0.1)
        # kill all client threads
        for key in self.clients:
            key.close()
            self.clients[key]["thread"].join(0.1)
            self.onclose(address=self.clients[key]["address"])
            self.__remove_client_from_dict(key)
        
        self.serversocket.close()
        self.clients.clear()
    
    #def send(self, address: str, data: str):
    def send(self, address, data):
        """
        send data to the connected node
        """
        
        clientsocket = self.__get_clientsocket_by_address(address)
        # running state for the client socket
        clientrunning = True
        # count the data already sent
        totalsent = 0
        # loop until all data has been transmitted
        while clientrunning and totalsent < len(data):
            # fire the data
            sent = clientsocket.send(data[totalsent:])
            # if nothing has been sent the socket is broken
            if sent == 0:
                self.onclose(address)
                self.__remove_client_from_dict(clientsocket)
                clientrunning = False
